/**
 * LayaDcc2 资源校验和版本控制脚本
 *
 * 该脚本用于为Laya项目生成资源校验文件，实现资源的版本控制和完整性校验。
 *
 * 主要功能包括：
 * 1. 资源校验文件生成：为项目资源生成DCC(Digest Code Check)校验文件
 * 2. CRC校验码计算：为每个资源文件计算路径CRC和内容CRC
 * 3. MD5摘要生成：为所有资源生成整体的MD5摘要标识
 * 4. 版本控制：通过校验码确保资源的一致性和完整性
 * 5. 文件类型统计：统计项目中各种文件类型的数量
 * 6. 缓存文件生成：可选生成资源缓存文件
 * 7. 路径标准化：支持路径转小写和空格编码处理
 *
 * 生成的文件包括：
 * - filetable.bin: 二进制格式的文件校验表
 * - allfiles.txt: 所有文件列表
 * - assetsid.txt: 资源整体MD5标识
 * - filetable.txt: 文本格式的文件校验表
 * - filetable1.txt: 内容校验表
 *
 * 工作流程：
 * - 在打包流程结束前自动执行
 * - 遍历所有项目资源文件
 * - 计算每个文件的CRC校验码
 * - 生成整体MD5摘要
 * - 创建校验文件并保存到update目录
 * - 支持将生成的校验文件上传到OSS
 *
 * 适用场景：
 * - Laya游戏项目资源版本管理
 * - 资源完整性校验
 * - 增量更新支持
 * - 防止资源被篡改
 */
package com.script

import com.Crc32.crc32
import com.Crc32.crc32Hex
import com.CustomScript
import com.entity.PackPath
import com.entity.toPackPath
import com.utils.PropertiesUtils
import com.utils.ScriptUtils.appConfig
import com.utils.ScriptUtils.contrastFile
import com.utils.ScriptUtils.script
import extend.equalsAny
import extend.printLog
import org.apache.commons.codec.digest.DigestUtils
import org.apache.logging.log4j.kotlin.logger
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.file.attribute.FileTime
import java.util.*
import kotlin.io.path.absolutePathString
import kotlin.io.path.isDirectory
import kotlin.math.max

private val cacheName = "layadcc.properties"

appConfig.ignoreList.add(cacheName)

private val propertiesUtils = PropertiesUtils()

val allfiles = mutableListOf<String>()
var assetsid = ""
var filetableBin: ByteBuffer? = null
val filetable = linkedMapOf<String, String>()
val filetable1 = linkedMapOf<String, String>()

script.customScript.add(object : CustomScript {

    override fun onBeforeClose() {
        super.onBeforeClose()
        propertiesUtils.store()
    }

    override fun onBeforeSubpackageEnd() {
        super.onBeforeSubpackageEnd()
        logger.info("---------------- 开启执行Laya生成校验文件 ----------------")

        propertiesUtils.read(appConfig.global.cachePath.resolve(cacheName).toPath)

        val deleteList = contrastFile.deleteOssPush
        val addList = contrastFile.addOssPush

        deleteList.forEach {
            logger.debug("上传资源缓存删除 $it")
            propertiesUtils.remove(it)
        }
        addList.forEach {
            propertiesUtils.add(it, it.crc())
        }

        val set = propertiesUtils.keySet()
        val current = mutableSetOf<PackPath>()
        set.forEach {
            val path = PackPath(it)
            current.add(path)
        }
        layadcc(appConfig.outDir.absolutePathString, current)

    }

})

private fun layadcc(outDir: String, currentPaths: MutableSet<PackPath>) {

    // 数据不对  重新生成完整的
    allfiles.clear()
    assetsid
    filetable.clear()
    filetable1.clear()
    filetableBin = null

    logger.info("执行LayaDcc资源生成")

    gendcc(outDir, files = currentPaths)

}

var excludes = mutableListOf(".git", ".svn", "update", "dccTools", "layadccout")
var excludeExts = mutableMapOf(".exportjson" to 1, ".pngz" to 1, ".jpgz" to 1, ".jngz" to 1)
//统计文件类型
var extfilest: MutableMap<String, Int> = HashMap()
var cnfg_newtxtfmt = true
var cnfg_newbinfmt = true

/**
 * @param outpath    输出目录。如果指定了url，则就输出在当前目录下。
 * @param genCache   是否生成cache
 * @param urlToLower 是否把资源文件名和路径全部转成小写。
 * @param url        如果生成资源的话，对应的url是什么。可以为空
 */
@OptIn(ExperimentalUnsignedTypes::class)
private fun gendcc(
    outpath: String = "",
    genCache: Boolean = false,
    urlToLower: Boolean = false,
    url: String? = null,
    options: Options? = null,
    files: MutableSet<PackPath> = mutableSetOf()
) {

    var outpath = outpath
    val bindcc = mutableListOf<UInt>()
    bindcc.add(0xffeeddccu) //标志和版本号
    bindcc.add(1u) //标志和版本号
    bindcc.add(0x00ffffffu) //由于已经发布的程序不能正确做到兼容版本，所以上面的版本号当成标志了。
    bindcc.add(1u) //这个才算真正版本
    //下面保存assetsid的md5
    val md5startPos = bindcc.size
    for (i in 0..7)  //4*8=32
        bindcc.add(0u) //先留空间

    if (genCache && !url.isNullOrEmpty()) {
        var cacheOut = url
        cacheOut = cacheOut.replace("http://", "")
        var index = cacheOut.indexOf('?')
        if (index > 0) {
            cacheOut = cacheOut.take(index)
        }
        index = cacheOut.lastIndexOf('/')
        if (index > 0) {
            cacheOut = cacheOut.take(index)
        }
        cacheOut = cacheOut.replace(":", ".")
        cacheOut = cacheOut.replace("\\", "_")
        cacheOut = cacheOut.replace("/", "_")

//        cacheOut = outpath + "cache" + cacheOut
//        var outFile = Path(outpath).createDirectories()

        outpath = "$outpath/$cacheOut"
        PackPath(outpath).createDirectories()
    }

    val outc = linkedMapOf<String, String>()
    val modifytm = true
    val outputStream = ByteArrayOutputStream()
    val finalOutpath = outpath

    for (file in files) {
        val fileStr = file.absolutePathString
        if (modifytm) {
            file.setLastModifiedTime(FileTime.fromMillis(System.currentTimeMillis()))
        }
        var relpath = contrastFile.getRelativePath(fileStr, true)!!
        if (urlToLower) relpath = relpath.lowercase(Locale.getDefault()) //转成小写
        if (options != null && options.escspace) {
            relpath = relpath.replace("\\s".toRegex(), "%20")
        }
        allfiles.add(relpath)
        val pathcrcv = crc32(relpath)
        bindcc.add((pathcrcv * 1).toUInt())
        var pathcrc = Integer.toHexString(pathcrcv)
        val padd = 8 - pathcrc.length
        if (padd > 0) {
            pathcrc = "00000000".take(padd) + pathcrc
        }
        //计算校验的时候统一把\r\n换成\n
        val ext = file.extension.lowercase()
        var content = propertiesUtils.get(fileStr)
        val contentcrc = content?.toUInt(16) ?: crc32(file.readBytes()).also {
            // 将内容CRC转换为十六进制字符串
            content = Integer.toHexString(it.toInt())
        }

        bindcc.add(contentcrc * 1u)
        // 将路径CRC和内容CRC添加到输出列表中
        outc[pathcrc] = content!!
        filetable1[pathcrc] = content!!

        // 读取文件内容
        val inbuf = file.readBytes()
//        println("$ext " + file.absolutePath)
        if (ext.equalsAny("txt", "html", "js", "exportjson", "css", "htm", "ini", "log")) {
            var tempBuff = ByteArray(inbuf.size)
            var si = 0
            var di = 0
            val srclen = inbuf.size
            // 处理文件内容，将\r\n替换为\n
            while (si < srclen) {
                if (inbuf[si].toInt() == 0x0d && si + 1 < srclen && inbuf[si + 1].toInt() == 0x0a) {
                    si++
                    si++
                    tempBuff[di++] = 0x0a.toByte()
                } else {
                    tempBuff[di++] = inbuf[si++]
                }
            }
            // 重新分配数组以适应实际内容大小
            tempBuff = tempBuff.copyOf(di)
            // 更新内容CRC
            content = crc32Hex(tempBuff)
            // 更新路径CRC和内容CRC到输出列表中
            filetable1[pathcrc] = content!!

        }

        //log( pathcrc+' '+content+'   \t'+relpath)
        //copy
        // 写入输出流

        outputStream.write(inbuf)
        // 如果需要生成缓存，将文件内容写入最终输出路径
        if (genCache) PackPath(finalOutpath, pathcrc).writeBytes(inbuf)
        //log(relpath)
    }

    // 计算并获取MD5哈希值
    assetsid = DigestUtils.md5Hex(outputStream.toByteArray())
    // 将MD5哈希值转换为整数数组
    val md5int = getUintArrFromMD5Str(assetsid)
    var stp = md5startPos
    // 将MD5整数数组的值填充到绑定数组中
    for (i in md5int.indices) {
        val v = md5int[i]
        bindcc[stp++] = v
    }
    // 如果MD5长度为32且配置了新文本格式，生成特定格式的输出文本文件
    if (assetsid.length == 32 && cnfg_newtxtfmt) {
        filetable["ffeeffee"] = "1"
        filetable[assetsid.substring(0, 8)] = assetsid.substring(8, 16)
        filetable[assetsid.substring(16, 24)] = assetsid.substring(24, 32)
    } else {
        // 如果MD5长度不为32，打印错误信息并生成普通的输出文本文件
        System.err.println("error! md5 length should be 32")
    }
    filetable.putAll(outc)

    // 写二进制的dcc
    // 写入二进制的filetable数
    filetableBin = ByteBuffer.allocate(4 * bindcc.size)
        .order(ByteOrder.LITTLE_ENDIAN)
    for (i in bindcc.indices) {
        filetableBin!!.putInt(bindcc[i].toInt())
    }

    saveDccFiles()

    allfiles.forEach {
        extfilest.merge(it.substring(it.lastIndexOf(".") + 1), 1) { a, b -> Integer.sum(a, b) }
    }

    addAddUpdate(outpath)

    // 打印扩展名统计信息和输出路
    printLog("总数: " + allfiles.size)
    printLog(extfilest)
    printLog("输出: $outpath")

}

private fun saveDccFiles() {

    // 保存更新后的数据到5个文件

    // 定义输出文件路径
    val savePath = appConfig.outDir.resolve("update")
    savePath.createDirectories()
    // 保存 filetable.bin
    savePath.resolve("filetable.bin").writeBytes(filetableBin!!.array())

    // 保存 allfiles.txt
    savePath.resolve("allfiles.txt").writeText(allfiles.joinToString(System.lineSeparator()))

    // 保存 assetsid.txt (需要实际重新计算)
    savePath.resolve("assetsid.txt").writeText(assetsid)

    // 保存 filetable.txt
    savePath.resolve("filetable.txt").writeText(
        filetable.map { "${it.key} ${it.value}" }.joinToString(System.lineSeparator())
    )

    // 保存 filetable1.txt
    savePath.resolve("filetable1.txt").writeText(
        filetable1.map { "${it.key} ${it.value}" }.joinToString(System.lineSeparator())
    )

    logger.info("保存更新后的DCC文件完成")

}

/**
 * 从MD5字符串获取无符号整数数组
 * @param s MD5字符串
 * @return 无符号整数数组
 */
@OptIn(ExperimentalUnsignedTypes::class)
private fun getUintArrFromMD5Str(s: String): UIntArray {
    val len = max(s.length, 32)
    val charBuf = UIntArray(len / 4)
    var i = 0
    while (i < s.length) {
        charBuf[i / 4] =
            fromBytes(s[i + 3].code.toByte(), s[i + 2].code.toByte(), s[i + 1].code.toByte(), s[i].code.toByte())
        i += 4
    }
    return charBuf
}

/**
 * 从四个字节构建一个整数
 * @param b1 第一个字节
 * @param b2 第二个字节
 * @param b3 第三个字节
 * @param b4 第四个字节
 * @return 构建的整数
 */
private fun fromBytes(b1: Byte, b2: Byte, b3: Byte, b4: Byte): UInt {
    return b1.toUInt() shl 24 or (b2.toUInt() and 0xFFu shl 16) or (b3.toUInt() and 0xFFu shl 8) or (b4.toUInt() and 0xFFu)
}


// 执行打包layadcc
private fun packLayadcc() {
    var cmdStr = appConfig.global.uploadParent
    if (script.property.containsKey("packLayadccPath"))
        cmdStr = PackPath(script.property["packLayadccPath"] as String)

    logger.info("gameExtend-> 执行打包layadcc $cmdStr")
    contrastFile.exec("layadcc", cmdStr!!.pathString)
    addAddUpdate(cmdStr.pathString)
}

private fun addAddUpdate(cmdStr: String) {
    if (!appConfig.runEnvironment().isUpload)
        return
    val file = PackPath(cmdStr, "update")

    val ossPush = contrastFile.ossPush
    val addOssPush = contrastFile.addOssPush
    if (file.exists()) {
        val files = file.walk().filter { !it.isDirectory() }.toList()
        if (files.isNotEmpty()) {
            for (i in ossPush.indices step 2) {
                if (ossPush[i] == "addFile") {
                    for (file in files) {
                        val exists = addOssPush.any {
                            it.absolutePathString == file.absolutePathString().replace("\\", "/")
                        }
                        if (!exists)
                            addOssPush.add(file.toPackPath)
                    }
                    ossPush[i + 1] = addOssPush.joinToString(",")
                    return
                }
            }
            for (file in files) {
                val exists = addOssPush.any {
                    it.absolutePathString == file.absolutePathString().replace("\\", "/")
                }
                if (!exists)
                    addOssPush.add(file.toPackPath)
            }
            ossPush.add("addFile")
            ossPush.add(addOssPush.joinToString(","))
        }
    }
}

private data class Options(
    /**
     * 文件路径全部转为小写
     */
    var lwr: Boolean = false,
    /**
     * 生成cache
     */
    var cache: Boolean = false,
    /**
     * url 生成cache的话，对应的url
     */
    var url: String? = null,
    /**
     * outpath cache的输出目录，如果不设置的话，就是在资源目录下。
     */
    var cout: Int = 0,
    /**
     * 将空格转换成%20
     */
    var escspace: Boolean = false
)